package com.tom.baselibrary.fixBug;

import android.content.Context;
import android.os.FileUtils;
import android.util.Log;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.List;

import dalvik.system.BaseDexClassLoader;

/**
 * Dex修复，管理类：
 * 0. 参考源码：BaseDexClassLoader
 * 1. 先拿到应用已经运行的 dexElement
 * 2. 获取下载好的补丁的   dexElement
 * 3. 把补丁的dexElement 插入到已经运行的 dexElement的最前面
 */
public class FixDexManager {
    private static final String TAG = "FixDexManager";
    private Context mContext;
    private File mDexDir; // 私有目录，复制到这个目录，系统才能访问

    public FixDexManager(Context context) {
        this.mContext = context;
        // 获取应用可访问的 dex目录
        this.mDexDir = context.getDir("odex", Context.MODE_PRIVATE);
    }

    /**
     * 修复dex包
     * @param fixDexPath  待修复 dexFilePath 绝对路径，本地下载好的待修复dex包。
     * @throws Exception
     */
    public void fixDex(String fixDexPath) throws Exception {
        // 2. 获取已经下载好的补丁的 dexElement
        // 2.1 移动到系统能访问的 dex目录下，  最终要变成ClassLoader
        File srcFile = new File(fixDexPath); // 源路径
        if (!srcFile.exists()) {
            throw new FileNotFoundException(fixDexPath);
        }
        File destFile = new File(mDexDir, srcFile.getName());
        if (destFile.exists()) { // 文件已经存在
            Log.d(TAG, "patch [" + fixDexPath + "] has been loaded.");
            return;
        }
        copyFile(srcFile, destFile); //文件拷贝，将已修复dex文件，统一拷贝到 mDexDir目录中。

        // 2.2 classLoader 读取fixDex的路径。为什么要加入集合，一启动可能就要修复
        List<File> fixDexFiles = new ArrayList<File>();
        fixDexFiles.add(destFile);

        fixDexFiles(fixDexFiles);
    }

    /**
     * 把合并后的 dexElements 注入到 ClassLoader里面。
     *
     * @param classLoader
     * @param dexElements 修复后的dex列表。
     */
    private void injectDexElements(ClassLoader classLoader, Object dexElements) throws Exception {
        // 1. 获取pathList
        Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList");
        pathListField.setAccessible(true);
        Object pathList = pathListField.get(classLoader);

        // 2. 获取pathList里面的 dexElements
        Field dexElementsField = pathList.getClass().getDeclaredField("dexElements");
        dexElementsField.setAccessible(true);
        dexElementsField.set(pathList, dexElements); // 注入：给dexElements 属性，设置新的列表值dexElements(修复生效)
    }


    // 合并数组的方法
    private static Object combineArray(Object arrayLhs, Object arrayRhs) {
        // 反射确定类型
        Class<?> localClass = arrayLhs.getClass().getComponentType();
        int i = Array.getLength(arrayLhs);
        int j = i + Array.getLength(arrayRhs);
        // 新数组
        Object result = Array.newInstance(localClass, j);
        for (int k = 0; k < j; ++k) {
            if (k < i) {
                Array.set(result, k, Array.get(arrayLhs, k));
            } else {
                Array.set(result, k, Array.get(arrayRhs, k - i));
            }
        }
        return result;
    }

    // copy文件。
    public static void copyFile(File src, File dest) throws IOException {
        FileChannel inChannel = null;
        FileChannel outChannel = null;
        try {
            if (!dest.exists()) {
                dest.createNewFile();
            }
            inChannel = new FileInputStream(src).getChannel();
            outChannel = new FileOutputStream(dest).getChannel();
            inChannel.transferTo(0, inChannel.size(), outChannel);
        } finally {
            if (inChannel != null) {
                inChannel.close();
            }
            if (outChannel != null) {
                outChannel.close();
            }
        }
    }


    /**
     * 加载全部的修复包
     */
    public void loadFixedDexes() throws Exception {
        File[] dexFiles = mDexDir.listFiles(); // 所有复制过来的dex存放路径：mDexDir
        List<File> fixDexFiles = new ArrayList<>();
        for (File dexFile : dexFiles) {
            if (dexFile.getName().endsWith(".dex")) {
                fixDexFiles.add(dexFile);
            }
        }
        // 修复dex
        fixDexFiles(fixDexFiles);
    }

    // 修复dex
    private void fixDexFiles(List<File> fixDexFiles) throws Exception {
        // 1. 先获取已经运行的 dexElement
        ClassLoader appClassLoader = mContext.getClassLoader();
        Object appDexElements = getDexElementsByClassLoader(appClassLoader);
        // 解压的路径
        File optimizedDirectory = new File(mDexDir, "odex");
        if (!optimizedDirectory.exists()) {
            optimizedDirectory.mkdirs();
        }

        // 3.去修复。
        for (File fixDexFile : fixDexFiles) {
            ClassLoader fixDexClassLoader = new BaseDexClassLoader(
                    fixDexFile.getAbsolutePath(), // dex路径：必须要在应用目录下的dex 文件中。
                    optimizedDirectory, // 解压路径
                    null, // .so文件的位置
                    appClassLoader // 父的classLoader
            );
            // 从ClassLoader 中获取 DexElements
            Object fixDexElements = getDexElementsByClassLoader(fixDexClassLoader);
            // 3.1 合并数组：把补丁的dexElement 插入到已经运行 DexElement的最前面。从而达到修复的目的。
            // 去合并。appClassLoader 数组  合并 fixDexElements 数组。
            // fixDexElements 放在最前面。
            appDexElements = combineArray(fixDexElements, appDexElements);
        }
        // 3.2 合并完成后的数组，注入到原来的类appClassLoader中。
        injectDexElements(appClassLoader, appDexElements);
    }

    /**
     * 从ClassLoader 中获取 DexElements
     * * 从BaseDexClassLoader 类中获取属性字段
     * @param classLoader
     * @return
     * @throws Exception
     */
    private Object getDexElementsByClassLoader(ClassLoader classLoader) throws
            Exception {
        // 1. 获取pathList：通过反射获取 pathList 属性。
        Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList");
        pathListField.setAccessible(true);
        Object pathList = pathListField.get(classLoader);

        // 2. DexElements:: 获取pathList里面的 dexElements
        Field dexElementsField = pathList.getClass().getDeclaredField("dexElements");
        dexElementsField.setAccessible(true);
        return dexElementsField.get(pathList);
    }

}
